package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.MyLockType;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author llt
 * @since 2024-08-07
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService exchangeCodeService;

    private final RedissonClient redissonClient;

    private final ICouponScopeService couponScopeService;

    @MyLock(name = "lock:coupon:uid:#{userId}", lockType = MyLockType.RE_ENTRANT_LOCK)
    public void receiveCoupon(Long id) {
        Long userId = UserContext.getUser();
        // 1. 根据id查询优惠券信息做校验
        if (id == null) {
            throw new BadRequestException("非法参数");
        }
        Coupon coupon = couponMapper.selectById(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        if (coupon.getStatus() != CouponStatus.ISSUING) {
            throw new BadRequestException("该优惠券未发放");
        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("未发放或者超过领取时间");
        }
        if (coupon.getTotalNum() <= 0 || coupon.getIssueNum() >= coupon.getTotalNum()) {
            throw new BadRequestException("库存不足");
        }

        // 2. 通过 aop 切面使用 redisson 实现分布式锁
        // 从 aop 上下文中 获取当前类的代理对象
        IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
        userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);

        // 不使用分布式锁
        /*
        synchronized (UserContext.getUser().toString().intern()) {
            // 从 aop 上下文中 获取当前类的代理对象
            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
            userCouponServiceProxy.checkAndCreateUserCoupon(UserContext.getUser(), coupon, null);
        }
         */

        // 通过工具类实现分布式锁
        /*
        // 2. 检验并生成用户券（里面有操作用户券和优惠券不同数据库的操作）
        String key = "lock:coupon:uid:" + userId;
        // 2.1 创建锁对象
        RedisLock redisLock = new RedisLock(key, redisTemplate);
        try {
            // 2.2 获取锁
            boolean isLock = redisLock.tryLock(5, TimeUnit.SECONDS);
            // 2.3 判断是否获取成功
            if (!isLock) {
                throw new BizIllegalException("操作频繁");
            }
            // 2.4 从 aop 上下文中 获取当前类的代理对象
            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);
        }finally {
            // 2.5 释放锁
            redisLock.unlock();
        }
        */

        // 通过 redisson 实现分布式锁
        /*
        String key = "lock:coupon:uid:" + userId;
        // 2.1 创建锁对象
        RLock lock = redissonClient.getLock(key);
        try {
            // 2.2 获取锁
            boolean isLock = lock.tryLock(); // 不加过期时间会生效看门狗机制, 默认时间是30s
            // 2.3 判断是否获取成功
            if (!isLock) {
                throw new BizIllegalException("操作频繁");
            }
            // 2.4 从 aop 上下文中 获取当前类的代理对象
            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);
        }finally {
            // 2.5 释放锁
            lock.unlock();
        }
        */
    }

    @Override
    @Transactional
    public void exchangeCpupon(String code) {
        // 1. 校验code是否为空
        if (StringUtils.isBlank(code)) {
            throw new BadRequestException("非法参数");
        }

        // 2. 解析兑换码得到自增id
        long serialNum = CodeUtil.parseCode(code); // 自增id

        // 3. 判断兑换码是否被兑换（bitmap）
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum, true);
        if (result) {
            // 已经被兑换
            throw new BizIllegalException("兑换码已被使用");
        }
        try {
            // 4. 判断兑换码是否存在（根据自增id）
            ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }

            // 5. 判断是否过期
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            if (now.isAfter(expiredTime)) {
                throw new BizIllegalException("兑换码已过期");
            }
            // 6. 校验并生成用户卷
            Long exchangeTargetId = exchangeCode.getExchangeTargetId();
            Coupon coupon = couponMapper.selectById(exchangeTargetId);
            if (coupon == null) {
                throw new BizIllegalException("优惠券不存在");
            }
            checkAndCreateUserCoupon(UserContext.getUser(), coupon, serialNum);
        }catch (Exception e) {
            // 7. 将redis中兑换码状态重置
            exchangeCodeService.updateExchangeCodeMark(serialNum, false);
            throw e;
        }

    }

    @Transactional
    public void checkAndCreateUserCoupon(Long userId, Coupon coupon, Long serialNum) {
        // 1. 获取当前用户领取该卷的数量
        Integer count = this.lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("您已经领取过量啦");
        }

        // 2. 优惠券已发放数量+1
        couponMapper.incrIssueNum(coupon.getId());

        // 3. 生成用户卷
        saveUserCoupon(UserContext.getUser(), coupon);

        if (serialNum != null) {
            // 4. 更新兑换码状态
            exchangeCodeService.lambdaUpdate()
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .set(ExchangeCode::getUserId, userId)
                    .eq(ExchangeCode::getId, serialNum)
                    .update();
        }

    }

    // 保存用户卷
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(coupon.getId());
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null && termEndTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);

        this.save(userCoupon);
    }

    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> courses) {
        // 1. 查询当前用户可用的优惠券（coupon && user_coupon）]
        List<Coupon> coupons = getBaseMapper().queryMyCoupons(UserContext.getUser());
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }

        // 2. 初筛(判断是否能在金额范围内使用)
        // 2.1 计算订单总金额
        int totalAmount = courses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        // 2.2 校验优惠券是否可用
        List<Coupon> availableCoupons = coupons.stream()
                .filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon))
                .collect(Collectors.toList());
        if (CollUtils.isEmpty(availableCoupons)) {
            return CollUtils.emptyList();
        }

        // 3. 细筛, 组合
        // 3.1 细筛
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons, courses);
        if (avaMap.isEmpty()) {
            return CollUtils.emptyList();
        }
        availableCoupons = new ArrayList<>(avaMap.keySet()); // 真正可可用的优惠券集合
        // 3.2 组合(能用的卷组合)
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons); // 不同的组合方案
        for (Coupon availableCoupon : availableCoupons) {
            solutions.add(List.of(availableCoupon)); // 添加单个卷到方案中
        }

        // 4. 计算每种组合的优惠明细
        /*
        List<CouponDiscountDTO> dtos = new ArrayList<>();
        for (List<Coupon> solution : solutions) {
            CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
            dtos.add(dto);
        }
        */

        // 5. 使用多线程优化第四步(每种组合使用一个线程操作)
        List<CouponDiscountDTO> dtos = Collections.synchronizedList(new ArrayList<>(solutions.size()));// 线程安全集合
        CountDownLatch latch = new CountDownLatch(solutions.size());
        for (List<Coupon> solution : solutions) {
            CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
                @Override
                public CouponDiscountDTO get() {
                    CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
                    return dto;
                }
            }).thenAccept(new Consumer<CouponDiscountDTO>() {
                @Override
                public void accept(CouponDiscountDTO dto) {
                    dtos.add(dto);
                    latch.countDown();// 计算减一
                }
            });
        }
        try {
            latch.await(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("多线程计算组合优惠明细失败", e);
        }

        // 6. 筛选最优结
        return findBestSolution(dtos);
    }

    /**
     * 求最优解
     * @param solutions
     * @return
     */
    private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
        // 1. 创建两个map 分别记录用卷相同, 金额最高. 金额相同, 用卷最少
        Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();

        // 2. 循环方案
        for (CouponDiscountDTO solution : solutions) {
            // 2.1 对优惠券 id 排序, 转字符串, 以","拼接
            String ids = solution.getIds()
                    .stream()
                    .sorted(Comparator.comparing(Long::longValue))
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
            // 2.2 从 moreDiscountMap 取旧的记录, 判断当前优惠金额 小于等于 旧的优惠金额就忽略
            CouponDiscountDTO old = moreDiscountMap.get(ids);
            if (old != null && solution.getDiscountAmount() <= old.getDiscountAmount()) {
                continue;
            }
            // 2.3 从 lessCouponMap 取旧的记录, 如果当前方案用卷数量 大于等于 旧的方案就忽略
            old = lessCouponMap.get(solution.getDiscountAmount());
            int curSize = solution.getIds().size(); // 当前方案用卷数量
            if (old != null && curSize > 1 && curSize >= old.getIds().size()) {
                continue;
            }
            // 2.4 添加最优方案到 map
            moreDiscountMap.put(ids, solution);
            lessCouponMap.put(solution.getDiscountAmount(), solution);
        }

        // 3. 求map交集
        Collection<CouponDiscountDTO> bestSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());

        // 4. 求最终的方案结果, 按照优惠金额降序
        List<CouponDiscountDTO> latestBestSolution = bestSolution
                .stream()
                .sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed())
                .collect(Collectors.toList());
        return latestBestSolution;
    }

    /**
     * 计算每组优惠券的优惠明细
     * @param avaMap 优惠券和可用课程的映射集合
     * @param courses 订单中所有的课程
     * @param solution 组合方案
     * @return
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> avaMap,
                                                        List<OrderCourseDTO> courses, List<Coupon> solution) {
        // 1. 创建方案结果 dto 对象
        CouponDiscountDTO dto = new CouponDiscountDTO();

        // 2. 初始化商品id和商品折扣明细的映射
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, orderCourseDTO -> 0));

        // 3. 计算该方案的折扣明细
        // 3.1 循环方案中优惠券
        for (Coupon coupon : solution) {
            // 3.2 取出优惠券对应的可用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            // 3.3 计算可用课程的总金额
            int totalAmount = availableCourses.stream()
                    .mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            // 3.4 判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (!discount.canUse(totalAmount, coupon)) {
                continue; // 优惠券不可用 跳出
            }
            // 3.5 计算优惠券使用后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.6 计算商品折扣明细
            calculateDetailDiscount(detailMap, availableCourses, totalAmount, discountAmount);
            // 3.7 累加计算总的优惠
            dto.getIds().add(coupon.getId());
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());
        }
        return dto;
    }

    /**
     * 计算商品折扣明细
     * @param detailMap 商品id和商品优惠明细
     * @param availableCourses 当前优惠券可用的课程集合
     * @param totalAmount 可用的课程的总金额
     * @param discountAmount 当前优惠券优惠的金额
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap, List<OrderCourseDTO> availableCourses,
                                         int totalAmount, int discountAmount) {
        // 循环可用商品
        int times = 0; // 已计算的商品个数
        int remainDiscount = discountAmount;

        for (OrderCourseDTO c : availableCourses) {
            times++;
            int discount = 0; // 每个券折扣的金额
            if (times == availableCourses.size()) {
                // 说明是最后一个课程
                discount = remainDiscount;
            } else {
                // 不是最后一个课程
                discount = c.getPrice() * discountAmount / totalAmount; // 先乘再除
                remainDiscount = discountAmount - discount;
            }
            // 将商品的折扣明细添加到 detailMap(可能多个券, 更新时要累加)
            detailMap.put(c.getId(), discount + detailMap.get(c.getId()));
        }

    }

    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons, List<OrderCourseDTO> courses) {
        Map<Coupon, List<OrderCourseDTO>> map = new HashMap<>();
        // 1. 遍历初筛后的优惠券集合
        for (Coupon coupon : coupons) {
            // 2. 找出每个优惠券的可用课程
            List<OrderCourseDTO> availableCourses = courses;
            // 2.1 判断优惠券是否限定了范围
            if (coupon.getSpecific()) {
                // 2.2 查询范围
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 2.3 得到可用的 id 集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                // 2.4 从订单课程中选出能用的
                availableCourses = courses.stream()
                        .filter(orderCourseDTO -> scopeIds.contains(orderCourseDTO.getCateId()))
                        .collect(Collectors.toList());
            }
            if (CollUtils.isEmpty(availableCourses)) {
                continue; // 当前优惠券限定了范围, 在订单中没有找到能用的.
            }

            // 3. 计算优惠券可用课程的总金额
            int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();

            // 4. 判断优惠券是否可用, 添加到map中
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalAmount, coupon)) {
                map.put(coupon, availableCourses);
            }
        }
        return map;
    }

}
